package com.example.composelauncher

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlin.coroutines.coroutineContext

class DragState(private val dragPadding: IntRect) {
    var appIcon: AppIcon? by mutableStateOf(null)
    var targetAnimator: ItemAnimator? = null
    val animateScale = Animatable(1f, Float.VectorConverter)
    private val dragChannel: Channel<DragEvent> = Channel(Channel.CONFLATED)
    var lastDragOverIndex: Int = -1
    private val reorderList: MutableList<ItemAnimator> = mutableListOf()
    private val animateLayout = mutableMapOf<AppIcon, ItemAnimator>()
    private var dragJob: Job? = null

    var lastWidth = 0
    var honrizontalGap = 0
    var verticalGap = 0
    var unitCount = 0
    var density: Density? = null

    inner class ItemAnimator(val appIcon: AppIcon, intOffset: IntOffset) {
        val animator : Animatable<IntOffset, AnimationVector2D> = Animatable(intOffset, IntOffset.VectorConverter)

        suspend fun animateToSlot(slot: Int) {
            animator.animateTo(staticLayout(slot))
        }
    }

    suspend fun dragBy(changePosition: Offset, position: Offset) {
        with(targetAnimator!!.animator) {
            /**
             * 向[targetAnimator]同步手指位置
             */
            snapTo(value + changePosition.round())
        }

        val target = findPointerTarget(position)
        if (target != lastDragOverIndex && target != -1) {
            if (lastDragOverIndex != -1) {
                /**
                 * 向[dragJob]发送[DragExit]事件
                 */
                dragChannel.send(DragExit(lastDragOverIndex))
            }
            lastDragOverIndex = target
            /**
             * 向[dragJob]发送[DragOver]事件
             */
            dragChannel.send(DragOver(target))
        }
    }

    /**
     * 开始拖拽
     * @param [appIcon] 待拖拽的图标
     * @param [appList] 包括[appIcon]在内的所有图标列表
     */
    suspend fun startDragging(appIcon: AppIcon, appList: List<AppIcon>) {
        this.appIcon = appIcon

        reorderList.clear()
        /**
         * 1. 初始化每一个图标的当前ItemAnimator
         * 2. 遍历所有列表找到[appIcon]的所在位置，为待拖拽的[targetAnimator]赋值
         * 3. 初始化当前顺序列表[reorderList]
         */
        appList.forEachIndexed { index, app ->
            ItemAnimator(app, staticLayout(index)).also {
                animateLayout[app] = it
                if (appIcon == app) {
                    targetAnimator = it
                }
                reorderList += it
            }
        }
        lastDragOverIndex = appList.indexOf(appIcon)

        val animScope = CoroutineScope(coroutineContext)
        // 新启协程响应拖拽事件
        dragJob = animScope.launch {
            // 先播放缩放动画
            animateScale.animateTo(1.18f)
            var emptySlot = lastDragOverIndex
            /**
             * 监听从[dragChannel]中收到的拖拽事件
             */
            while (true) {
                val event = dragChannel.receive()
                val targetSlot = event.index
                if (event !is DragOver || targetSlot == emptySlot
                    || targetSlot < 0 || targetSlot >= reorderList.size) {
                    continue
                }
                /**
                 * 代码跑到这儿表示收到了一个新的[DragOver]事件
                 */
                try {
                    /**
                     * 等待超时，若是在250ms内收到了[DragExit]则忽略此次[DragOver]事件
                     */
                    withTimeout(250L) {
                        while (true) {
                            val nextEvent = dragChannel.receive()
                            if (nextEvent is DragExit && nextEvent.index == targetSlot) {
                                break
                            }
                        }
                    }
                    /**
                     * 收到了[DragExit]事件，回到主循环等待下一次[DragOver]事件
                     */
                } catch (e: TimeoutCancellationException) {
                    /**
                     * 没能收到[DragExit]事件，表示用户的手指靠在这个图标格子上已经超过了250ms，我们开始正式播放reorder动画
                     */
                    if (targetSlot > emptySlot) {
                        for (i in emptySlot until targetSlot) {
                            val itemAnimator = reorderList[i + 1]
                            reorderList[i] = itemAnimator
                            /**
                             * 修改[reorderList]数据结构，记录最新的顺序，并且播放reorder动画
                             */
                            animScope.launch {
                                itemAnimator.animateToSlot(i)
                            }
                        }
                    } else {
                        for (i in emptySlot downTo targetSlot + 1) {
                            val itemAnimator = reorderList[i - 1]
                            reorderList[i] = itemAnimator
                            /**
                             * 修改[reorderList]数据结构，记录最新的顺序，并且播放reorder动画
                             */
                            animScope.launch {
                                itemAnimator.animateToSlot(i)
                            }
                        }
                    }
                    reorderList[targetSlot] = targetAnimator!!
                    /**
                     * 更新[emptySlot]，为下一次[DragOver]事件做准备
                     */
                    emptySlot = targetSlot
                }
            }
        }
    }

    suspend fun stopDragging() : List<AppIcon> {
        /**
         * 构建输出结果，即为记录着最新顺序的[reorderList]
         */
        val outList = reorderList.map {
            it.appIcon
        }
        /**
         * startDrag协程可能正在等待[DragExit]超时，我们提前让其结束超时等待
         */
        dragChannel.send(DragExit(lastDragOverIndex))
        coroutineScope {
            /**
             * 将被拖动的图标移回其reorder之后最新的位置
             */
            launch {
                targetAnimator!!.animateToSlot(outList.indexOf(appIcon))
            }
            /**
             * 将被拖动的图标恢复缩放大小
             */
            launch {
                animateScale.animateTo(1f)
            }
        }
        /**
         * 因为用了coroutineScope，所以当代码跑到这里，表示上面的两个动画已经播放结束了，
         * 此时[dragJob]正在无限循环中等待下一个[DragOver]事件，而这个事件已经不可能再有了，所以我们终止[dragJob]
          */
        dragJob?.cancel()
        dragJob = null
        appIcon = null
        lastDragOverIndex = -1
        reorderList.clear()
        animateLayout.clear()
        targetAnimator = null
        return outList
    }

    fun reportLayoutPort(width: Int) {
        density!!.apply {
            if (lastWidth != width) {
                lastWidth = width
                val space = width - dragPadding.left - dragPadding.right
                var preferUnitCount = space / unitWidth.roundToPx()
                var determinedGap: Int
                do {
                    determinedGap = ((space - preferUnitCount * unitWidth.roundToPx()) / (preferUnitCount + 1))
                        .coerceAtMost(preferHorizontalGapRange.second.roundToPx())
                    preferUnitCount--
                } while (determinedGap < preferHorizontalGapRange.first.roundToPx())
                unitCount = ++preferUnitCount
                honrizontalGap = determinedGap
                verticalGap = honrizontalGap.coerceAtLeast(preferVerticalGap.roundToPx())
            }
        }
    }

    fun staticLayout(index: Int) : IntOffset {
        return density!!.run {
            val unitXIndex = index % unitCount
            val unitYIndex = index / unitCount
            IntOffset(dragPadding.left + (unitXIndex + 1) * honrizontalGap + unitXIndex * unitWidth.roundToPx(),
                dragPadding.top + (unitYIndex + 1) * verticalGap + unitYIndex * unitHeight.roundToPx())
        }
    }

    fun isDraggingItem(app: AppIcon): Boolean {
        return app == appIcon
    }

    fun dynamicLayout(appIcon: AppIcon, index: Int) : IntOffset {
        return animateLayout[appIcon]?.animator?.value?: staticLayout(index)
    }

    fun measureHeight(size: Int): Int {
        return density!!.run {
            staticLayout(size - 1).y + unitHeight.roundToPx() + verticalGap + dragPadding.bottom
        }
    }

    private fun findPointerTarget(offset: Offset): Int {
        if (honrizontalGap <= 0) {
            return -1
        }
        return density!!.run {
            val unitWidthInPixel = unitWidth.roundToPx()
            val unitHeightInPixel = unitHeight.roundToPx()
            var slotInX = 0
            while (true) {
                val left = dragPadding.left + (slotInX + 1) * honrizontalGap + slotInX * unitWidthInPixel
                if (offset.x < left) {
                    return -1
                }
                if (offset.x <= left + unitWidthInPixel) {
                    break
                }
                slotInX++
            }
            var slotInY = 0
            while (true) {
                val top = dragPadding.top + (slotInY + 1) * verticalGap + slotInY * unitHeightInPixel
                if (offset.y < top) {
                    return -1
                }
                if (offset.y <= top + unitHeightInPixel) {
                    break
                }
                slotInY++
            }
            slotInX + slotInY * unitCount
        }
    }
}

sealed class DragEvent(val index: Int) {
    override fun toString(): String {
        return "DragEvent(index=$index)"
    }
}

class DragOver(index: Int): DragEvent(index = index)
class DragExit(index: Int): DragEvent(index = index)

fun Modifier.dragAndReorder(scrollState: ScrollState, channel: Channel<AppIcon>, dragState: DragState,
                            input: List<AppIcon>, reorderResult: (List<AppIcon>) -> Unit) : Modifier {
    return this.composed {
        val coroutineScope = rememberCoroutineScope()
        Modifier.pointerInput(input) {
            while (true) {
                val draggingAppIcon = channel.receive()
                dragState.startDragging(draggingAppIcon, input)

                awaitPointerEventScope {
                    while (true) {
                        awaitPointerEvent(PointerEventPass.Initial)
                        val pointerChange = currentEvent.changes.first { it.pressed || it.previousPressed }

                        val offsetChange = pointerChange.positionChange()
                        val position = pointerChange.position
                        coroutineScope.launch {
                            dragState.dragBy(offsetChange, position + Offset(0f, scrollState.value.toFloat()))
                        }

                        pointerChange.consumePositionChange()
                        if (pointerChange.changedToUpIgnoreConsumed()) {
                            break
                        }
                    }
                }

                reorderResult.invoke(dragState.stopDragging())
            }
        }
    }
}

@Composable
@ExperimentalFoundationApi
fun LauncherWorkspace(
    modifier: Modifier = Modifier,
    iconList: List<AppIcon>,
    dragPadding: IntRect = IntRect.Zero,
    onItemClick: ((AppIcon) -> Unit)? = null,
    reorderResult: (List<AppIcon>) -> Unit
) {
    val dragState = remember(dragPadding, iconList) {
        DragState(dragPadding)
    }
    dragState.density = LocalDensity.current

    val channel = remember {
        Channel<AppIcon>()
    }
    val scrollState = rememberScrollState()

    Layout(
        modifier = modifier
            .dragAndReorder(
                scrollState,
                channel, dragState, iconList,
                reorderResult = reorderResult
            )
            .verticalScroll(state = scrollState),
        content = {
            iconList.forEach {app ->
                AppIcon(
                    modifier = Modifier
                        .layoutId(app)
                        .size(unitWidth, unitHeight),
                    onLongClick = { channel.offer(app) },
                    appIcon = app,
                    onClick = onItemClick)
            }
        }
    ) { measurables, constraints ->
        var unitWidth = 0
        var unitHeight = 0
        val placeableMap = mutableMapOf<Placeable, AppIcon>()
        val childrenConstraints = constraints.copy(minWidth = 0, minHeight = 0)
        val placeables = measurables.map { measurable ->
                // Measure each child
                measurable.measure(childrenConstraints).also {
                    unitWidth = unitWidth.coerceAtLeast(it.width)
                    unitHeight = unitHeight.coerceAtLeast(it.height)
                }.also {
                    placeableMap[it] = measurable.layoutId as AppIcon
                }
            }

        dragState.reportLayoutPort(constraints.maxWidth)

        val heightEstimated = dragState.measureHeight(placeables.size).coerceAtLeast(constraints.minHeight)

        layout(constraints.maxWidth, heightEstimated) {

            placeables.forEachIndexed { index, placeable ->

                val appIcon = placeableMap[placeable]
                val layoutOffset = dragState.dynamicLayout(appIcon!!, index)

                if (dragState.isDraggingItem(appIcon)) {
                    placeable.placeRelativeWithLayer(position = layoutOffset, zIndex = 1f) {
                        with(dragState) {
                            scaleX = animateScale.value
                            scaleY = animateScale.value
                        }
                    }
                } else {
                    placeable.placeRelative(layoutOffset)
                }
            }
        }
    }
}

@ExperimentalFoundationApi
@Composable
fun AppIcon(modifier: Modifier = Modifier,
            onLongClick: ((AppIcon) -> Unit)? = null,
            appIcon: AppIcon,
            onClick: ((AppIcon) -> Unit)? = null) {

    val interactionSource = remember {
        MutableInteractionSource()
    }

    Column(
        modifier = modifier.combinedClickable(
            interactionSource = interactionSource,
            indication = LocalIndication.current,
            onLongClick = {
                interactionSource.tryEmit(PressInteraction.Release(PressInteraction.Press(Offset.Zero)))
                onLongClick?.invoke(appIcon)
            },
            onClick = {onClick?.invoke(appIcon)}
        ),
        horizontalAlignment = Alignment.CenterHorizontally) {

        Image(painter = BitmapPainter(appIcon.icon.asImageBitmap()),
            contentDescription = appIcon.title)

        Spacer(modifier = Modifier
            .weight(1f)
            .fillMaxWidth())

        Text(text = appIcon.title,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            color = MaterialTheme.colors.onPrimary)
    }
}

val unitWidth = 80.dp
val unitHeight = 80.dp
val preferHorizontalGapRange = 10.dp to 100.dp
val preferVerticalGap = 42.dp